;;; imenu-extra.el --- Add extra items into existing imenu items -*- lexical-binding: t -*-

;; Copyright (C) 2020 Chen Bin <chenbin DOT sh AT gmail DOT com>
;;
;; Version: 0.0.1
;; Keywords: convenience
;; Author: Chen Bin <chenbin DOT sh AT gmail DOT com>
;; URL: https://github.com/redguardtoo/imenu-extra
;; Package-Requires: ((emacs "25.1"))

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program; if not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;  Add extra items into existing imenu items generated by major/minor modes.
;;  For example, extra items could be appended into existing imenu items
;;  generated by lsp-mode and js2-mode.
;;
;;  Set up,
;;    At the end of minor/major mode hook, call `imenu-extra-auto-setup' which has
;;    one parameter "patterns".
;;    Patterns should be an alist of the same form as `imenu-generic-expression'.
;;    For example, insert below code to `js2-mode-hook' to extract unit test
;;    cases from javascript code.
;;
;; (add-hook 'js2-mode-hook
;;           (lambda ()
;;             ;; original js2-mode setup ...
;;
;;             ;; at the end of mode hook
;;             (require 'imenu-extra)
;;             (imenu-extra-auto-setup '(("tdd.it" "^[ \t]*it('\\([^']+\\)" 1)
;;                                       ("tdd.desc" "^[ \t]*describe('\\([^']+\\)" 1))))
;; Tips:
;;   - Set `imenu-extra-process-item-function' to process extra imenu items
;;
;; Usage,
;;   Use imenu as usual.
;;

;;; Code:

(require 'cl-lib)
(require 'imenu)

(defgroup imenu-extra nil
  "Add extra items into existing imenu items."
  :group 'tools)

(defcustom imenu-extra-process-item-function 'identity
  "Function to process extra imenu item."
  :type 'function
  :group 'imenu-extra)

(defun imenu-extra-line-range (position)
  "Get the line range at POSITION."
  (let (rlt)
    (save-excursion
      (goto-char position)
      (setq rlt (cons (line-beginning-position) (line-end-position))))
    rlt))

(defun imenu-extra-position (item)
  "Get imenu ITEM's position."
  (cond
   ((not item)
    nil)

   ((integerp item)
    item)

   ((markerp item)
    (marker-position item))

   ;; plist
   ((and (listp item) (listp (cdr item)))
    (imenu-extra-position (cadr item)))

   ;; alist
   ((and (listp item) (not (listp (cdr item))))
    (imenu-extra-position (cdr item)))))

;;;###autoload
(defun imenu-extra-add-new-items (original-items patterns)
  "Merge ORIGINAL-ITEMS and extra imenu items from PATTERNS.
PATTERNS should be an alist of the same form as `imenu-generic-expression'."
  ;; Clear the lines.
  (let* ((extra-items (save-excursion (imenu--generic-function patterns))))
    (cond
     (extra-items
      (let* (ranges)
        ;; Analyze the old imenu items from original imenu backend.
        ;; Only care about line number here.
        (dolist (item original-items)
          (let* ((val (imenu-extra-position item)))
            (when val
              (push (imenu-extra-line-range val) ranges))))

        ;; remove items already defined in original items
        (setq extra-items
              (cl-remove-if
               (lambda (item)
                 (let* ((position (imenu-extra-position item)))
                   (cl-some (lambda (item-range)
                              (and position
                                   (< position (cdr item-range))
                                   (>= position (car item-range))))
                            ranges)))
               extra-items))

        (setq extra-items
              (mapcar imenu-extra-process-item-function extra-items))
        ;; EXTRA-ITEMS sample:
        ;; ((function ("hello" . #<marker 63>) ("bye" . #<marker 128>))
        ;;  (controller ("MyController" . #<marker 128))
        ;;  (hello . #<marker 161>))
        (append original-items extra-items)))

     (t
      original-items))))

;;;###autoload
(defun imenu-extra-auto-setup (patterns)
  "Add extra imenu items extracted from PATTERNS.
PATTERNS should be an alist of the same form as `imenu-generic-expression'."
  (let* ((old-imenu-fn imenu-create-index-function))
    (setq imenu-create-index-function
          (lambda ()
            (imenu-extra-add-new-items (funcall old-imenu-fn) patterns)))))

(provide 'imenu-extra)
;;; imenu-extra.el ends here
